﻿/****************
* Authored by Zhuo "Wizcas" Chen
* 2018 © Wubi.fun all rights reserved
*****************/

using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Wubi.Utils;

public class BlockStack : MonoBehaviour, IList<Block>
{
    public float produceInterval = 5f;

    public Block mergedPrefab; //todo
    [SerializeField] MergeRequest _mergeRequest;

    #region Internal Fields
    List<Block> _blocks = new List<Block>();
    float _topY;
    bool _isDirty;
    bool _needCheckCombos;
    IEnumerable<Combo> _combos;
    long _prevBlocksHash;
    #endregion

    #region Properties
    [SerializeField] Vector2Int _coord;
    public Vector2Int coord
    {
        get { return _coord; }
        set
        {
            _isDirty = value != _coord;
            _coord = value;
        }
    }
    public int id { get { return coord.x * 17 + coord.y * 23; } }
    public CellInfo atCell { get { return Ground.instance.GetCellInfo(coord); } }
    public Vector3 topPos { get { return transform.position + Vector3.up * _topY; } }
    bool canMerge { get { return AnyCombo(_combos); } }
    AudioSource _ausrc;
    public AudioSource audioSrc { get { return _ausrc ?? (_ausrc = GetComponent<AudioSource>()); } }
    #endregion

    #region Events
    public System.Action<IEnumerable<Res>, BlockStack> onResChange;
    public System.Action<BlockStack> onStackChanged;
    #endregion

    #region Life Cycle
    void Update()
    {
        long blocksHash = _blocks.Sum(b => b.GetInstanceID());
        if(blocksHash != _prevBlocksHash)
        {
            _isDirty = true;
            _prevBlocksHash = blocksHash;
        }
        CheckCombos();
        CheckMergeRequest();
        UpdateStack();
        UpdateProduction();
    }
    #endregion
    
    #region List Ops
    public int Count { get { return _blocks.Count; } }
    public bool IsReadOnly { get { return false; } }
    public Block this[int index]
    {
        get { return _blocks[index]; }
        set { _blocks[index] = value; }
    }

    public void Clear()
    {
        for (int i = _blocks.Count - 1; i >= 0; i--)
        {
            if (_blocks[i] != null) _blocks[i].Kill();
        }
        _blocks.Clear();
        SetChanged();;
    }

    public void Add(Block block)
    {
        RemoveBlockFromPrevStack(block);
        _blocks.Add(block);
        block.transform.SetParent(transform, true);
        SetChanged();
    }

    public bool Remove(Block block)
    {
        var succ = _blocks.Remove(block);
        if (succ) SetChanged();;
        return succ;
    }

    public int IndexOf(Block item)
    {
        return _blocks.IndexOf(item);
    }

    public void Insert(int index, Block item)
    {
        RemoveBlockFromPrevStack(item);
        _blocks.Insert(index, item);
        item.transform.SetParent(transform, true);
        item.transform.SetSiblingIndex(index + 1);
        SetChanged();
    }

    public void RemoveAt(int index)
    {
        _blocks.RemoveAt(index);
        SetChanged();;
    }

    public bool Contains(Block item)
    {
        return _blocks.Contains(item);
    }

    public void CopyTo(Block[] array, int arrayIndex)
    {
        _blocks.CopyTo(array, arrayIndex);
    }

    public IEnumerator<Block> GetEnumerator()
    {
        return _blocks.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _blocks.GetEnumerator();
    }
    #endregion

    #region Stack Ops

    public static bool IsEmpty(BlockStack stack)
    {
        return stack == null || stack.Count == 0;
    }

    void SetChanged()
    {
        _isDirty = true;
        if (onStackChanged != null) onStackChanged(this);
    }

    void RemoveBlockFromPrevStack(Block block)
    {
        if (block.coord.HasValue)
        {
            var oldStack = World.instance.GetStack(block.coord.Value);
            if (oldStack != null) oldStack.Remove(block);
        }
    }

    void UpdateStack()
    {
        if (!_isDirty) return;
        _isDirty = false;
        var cell = atCell;
        if (!cell.isInGrid) return;
        transform.position = cell.pos;
        var y = 0f;
        foreach (var block in _blocks)
        {
            block.transform.localPosition = Vector3.up * y;
            y += block.height;
        }
        _topY = y;
    }

    bool CheckMergeRequest(){
        var ret = canMerge;
        BlockData blockData = null;
        if(ret){
            blockData = Database.instance.GetBlockData(_combos.First().outcome);
        }
        _mergeRequest.SetVisible(ret, blockData == null ? Res.empty : blockData.mergePrice.MakeRes());
        return ret;
    }

    public bool Merge()
    {
        if (Game.instance.isInTutorial || !CheckMergeRequest()) { return false; }
        var combo = _combos.First();
        // res checking
        var outcomeData = Database.instance.GetBlockData(_combos.First().outcome);
        if (outcomeData == null || outcomeData.mergePrice.isEmpty) return false;
        var priceRes = new[] { outcomeData.mergePrice.MakeRes() };
        var enoughRes = ResManager.instance.IsEnough(priceRes);
        if (!enoughRes)
        {
            ResPopper.instance.PopTip("Insufficient resources", this, Color.red);
            return false;
        }
        if (onResChange != null) onResChange(priceRes, this);
        // merging
        var newBlock = Block.Spawn(combo.outcome);
        var firstIndex = -1;
        foreach(var b in combo.matchedBlocks)
        {
            if (firstIndex < 0) firstIndex = b.index - 1;
            b.Kill();
        }
        if (firstIndex >= Count)
        {
            Add(newBlock);
        }
        else
        {
            Insert(firstIndex, newBlock);
        }
        _combos = null;
        return true;
    }

    public BlockStackMatrix GetMatrix()
    {
        return new BlockStackMatrix(this);
    }

    public void RequestCheckCombos() { _needCheckCombos = true; }
    void CheckCombos()
    {
        if (!_needCheckCombos) return;
        _needCheckCombos = false;
        var combos = Database.instance.GetMatchingCombos(GetMatrix());
        var hasCombo = AnyCombo(combos);
        if (hasCombo) { _isDirty = true; Debug.LogFormat("{0}@{1}", name, combos.Print("\n")); }
        _combos = combos;
    }

    bool AnyCombo(IEnumerable<Combo> combos)
    {
        return combos != null && combos.Any();
    }

    #endregion

    #region Production
    float _lastPTime;
    void UpdateProduction()
    {
        if(Game.instance.isStarted && !Game.instance.isInTutorial && Time.time >= _lastPTime + produceInterval)
        {
            Produce();
            _lastPTime = Time.time;
        }
    }
    void Produce()
    {
        Dictionary<string, Res> products = new Dictionary<string, Res>();
        foreach(var block in this)
        {
            var ps = block.Produce();
            if (ps == null) continue;
            foreach(var res in ps)
            {
                if (!res.isValid) continue;
                Res p;
                if(!products.TryGetValue(res.id, out p))
                {
                    p = res;
                }
                else
                {
                    p.amount += res.amount;
                }
                products[res.id] = p;
            }
        }
        if (onResChange != null)
            onResChange(products.Values, this);
    }
    #endregion
}

public class BlockStackMatrix
{
    BlockStack[,] _stacks = new BlockStack[3, 3];
    public BlockStackMatrix(BlockStack stack)
    {
        var coord = stack.coord;
        _stacks[0, 0] = World.instance.GetStack(new Vector2Int(coord.x - 1, coord.y - 1));
        _stacks[1, 0] = World.instance.GetStack(new Vector2Int(coord.x, coord.y - 1));
        _stacks[2, 0] = World.instance.GetStack(new Vector2Int(coord.x + 1, coord.y - 1));
        _stacks[0, 1] = World.instance.GetStack(new Vector2Int(coord.x - 1, coord.y));
        _stacks[1, 1] = stack;
        _stacks[2, 1] = World.instance.GetStack(new Vector2Int(coord.x + 1, coord.y));
        _stacks[0, 2] = World.instance.GetStack(new Vector2Int(coord.x - 1, coord.y + 1));
        _stacks[1, 2] = World.instance.GetStack(new Vector2Int(coord.x, coord.y + 1));
        _stacks[2, 2] = World.instance.GetStack(new Vector2Int(coord.x + 1, coord.y + 1));
    }

    public BlockStack o { get { return _stacks[1, 1]; } }
    public BlockStack o_sw { get { return _stacks[0, 0]; } }
    public BlockStack o_s { get { return _stacks[1, 0]; } }
    public BlockStack o_se { get { return _stacks[2, 0]; } }
    public BlockStack o_w { get { return _stacks[0, 1]; } }
    public BlockStack o_e { get { return _stacks[2, 1]; } }
    public BlockStack o_nw { get { return _stacks[0, 2]; } }
    public BlockStack o_n { get { return _stacks[1, 2]; } }
    public BlockStack o_ne { get { return _stacks[2, 2]; } }

    public IEnumerable<BlockStack> all
    {
        get {
            for(int r = 0; r < 3; r++)
            {
                for(int c = 0; c < 3; c++)
                {
                    yield return _stacks[c, r];
                }
            }
        }
    }
    public BlockStack[][] cross
    {
        get
        {
            return new[]
            {
                new []{o_w, o_e },
                new []{o_s, o_n}
            };
        }
    }
    public IEnumerable<BlockStack> surround
    {
        get
        {
            yield return o_sw; yield return o_s; yield return o_se;
            yield return o_w; yield return o_e;
            yield return o_nw; yield return o_n; yield return o_ne;
        }
    }
}